package de.upb.aquarium.user;

import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.HashMap;
import java.util.Map;

import de.upb.aquarium.World;
import de.upb.aquarium.api.utility.VectorDouble;

/**
 * A mouse listener for a 3D application. Keeps track of the current
 * view direction. The vertical view angle is limited to 85 degrees up and
 * down.
 * @author Jost
 */
public class CG2InputListener implements MouseMotionListener, MouseListener, KeyListener {

	private VectorDouble position;
	private VectorDouble direction;
	
	private double movementSpeed;
	private double rotationSpeed;

	private double currentAngleHorizontal;
	private double currentAngleVertical;
	
	private int previousX;
	private int previousY;
	
	Map<Character, Double> keyDownMap;
	Map<Character, Boolean> keyTypedMap;

	/**
	 * Creates a new input listener for CG 2.
	 * @param initialPosition The viewers initial position.
	 * @param initialDirection The viewers initial direction.
	 * @param movementSpeed The movement speed as a factor.
	 * @param rotationSpeed The rotation speed as a factor.
	 */
	public CG2InputListener(
			VectorDouble initialPosition,
			VectorDouble initialDirection,
			double movementSpeed,
			double rotationSpeed) {
		
		super();
		
		if (initialDirection.getSize() != 3 || initialPosition.getSize() != 3)
		{
			throw new IllegalArgumentException("Direction or position" +
					" vector was not of size 3");
		}
		
		position = new VectorDouble(initialPosition);
		direction = new VectorDouble(initialDirection);
		direction.normalize();
		
		this.movementSpeed = movementSpeed;
		this.rotationSpeed = rotationSpeed;
		
		calculateAnglesFromDirection();
		
		this.previousX = 0;
		this.previousY = 0;
		
		keyDownMap = new HashMap<Character, Double>();
		keyDownMap.clear();
		
		keyTypedMap = new HashMap<Character, Boolean>();
		keyTypedMap.clear();
	}

	@Override
	public void mousePressed(MouseEvent e) {
		previousX = e.getX();
		previousY = e.getY();
	}
	
	@Override
	public void mouseDragged(MouseEvent e) {
		
		
		int x = e.getX();
		int y = e.getY();
		
		Dimension size = e.getComponent().getSize();
		
		// Store thetaY movement for rotation around x-axis
		float thetaY = ((float) (x - previousX) / (float) size.width);
		// Store thetaX movement for rotation around y-axis
		float thetaX = ((float) (previousY - y) / (float) size.height);
		
		currentAngleHorizontal	+= thetaY*rotationSpeed;
		currentAngleVertical	+= thetaX*rotationSpeed;
		
		previousX = x;
		previousY = y;
		
		// Make sure that angles are in valid range and don't get to big or
		// small.
		currentAngleHorizontal %= 2.0*Math.PI;
		if (currentAngleVertical > (85.0/180.0)*Math.PI)
		{
			currentAngleVertical = (85.0/180.0)*Math.PI;
		}
		else if (currentAngleVertical < -(85.0/180.0)*Math.PI)
		{
			currentAngleVertical = -(85.0/180.0)*Math.PI;
		}
	}

	@Override
	public void mouseMoved(MouseEvent e) {

	}
	
	public boolean getKeyTyped(char keyChar)
	{
		boolean result = false;
		if (keyTypedMap.containsKey(keyChar))
		{
			result = keyTypedMap.get(keyChar).booleanValue();
			keyTypedMap.put(keyChar, false);
		}
		
		return result;
	}
	
	/**
	 * Helper for calculating the time a key has been pressed since last
	 * update. Resets the timer for that key.
	 * @param keyChar The key to check.
	 * @return The time in seconds the key was pressed since last update. 
	 */
	private double getKeyDownTime(char keyChar)
	{
		double result;
		if (keyDownMap.containsKey(keyChar) && (keyDownMap.get(keyChar) > 0.0))
		{
			result = World.getInstance().getElapsedTime(keyDownMap.get(keyChar));
			keyDownMap.put(keyChar, World.getInstance().getGlobalTime());
		}
		else
		{
			result = 0.0;
		}
		
		return result;
	}
	
	/**
	 * Computes and retrieves the position of the viewer.
	 * @return The players current position.
	 */
	public VectorDouble getPosition() {
		
		// Update direction vector
		calculateDirectionFromAngles();
		
		VectorDouble offset = new VectorDouble(new double[] {0.0, 0.0, 0.0});
		
		// How long was which key down?
		double timeWDown = getKeyDownTime('w');
		double timeADown = getKeyDownTime('a');
		double timeSDown = getKeyDownTime('s');
		double timeDDown = getKeyDownTime('d');

		// Calculate a vector which points left.
		VectorDouble up = new VectorDouble(new double[] {0.0, 1.0, 0.0});
		VectorDouble left = up.CrossProduct(direction);
		
		// Calculate the offset.
		offset = offset.add(direction.mul(timeWDown));
		offset = offset.sub(direction.mul(timeSDown));
		offset = offset.add(left.mul(timeADown));
		offset = offset.sub(left.mul(timeDDown));
		
		offset = offset.mul(movementSpeed);
		
		// Modifiy the position according to the offset.
		position = position.add(offset);
		
		return position;
	}

	/**
	 * Computes and retrieves the viewers current viewing direction.
	 * @return The current viewing direction.
	 */
	public VectorDouble getDirection() {
		calculateDirectionFromAngles();
		return direction;
	}
	
	/**
	 * Helper to make the conversation "direction angles -> direction vectors".
	 */
	private void calculateDirectionFromAngles()
	{
		double dirX = Math.sin(currentAngleHorizontal);
		double dirY = Math.sin(currentAngleVertical);
		double dirZ = -Math.cos(currentAngleHorizontal)*Math.cos(currentAngleVertical);
		
		direction.data[0] = dirX;
		direction.data[1] = dirY;
		direction.data[2] = dirZ;
		
		direction.normalize();
	}

	/**
	 * Helper to make the conversation "direction vectors -> direction angles".
	 */
	private void calculateAnglesFromDirection()
	{		
		double dirX = direction.data[0];
		double dirY = direction.data[1];
		double dirZ = direction.data[2];
		
		double b = Math.sqrt(dirX*dirX + dirZ*dirZ);
		currentAngleVertical = Math.acos(b);
		
		double a = Math.sqrt(dirY*dirY + dirZ*dirZ);
		currentAngleHorizontal = Math.acos(a);
	}

	@Override
	public void keyPressed(KeyEvent e) {		
		keyDownMap.put(e.getKeyChar(), World.getInstance().getGlobalTime());
	}

	@Override
	public void keyReleased(KeyEvent e) {
		// Update position:
		getPosition();
		keyDownMap.put(e.getKeyChar(), -1.0);		
	}

	@Override
	public void keyTyped(KeyEvent e) {
		keyTypedMap.put(e.getKeyChar(), true);
	}

	@Override
	public void mouseClicked(MouseEvent e) {
		
		
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		
		
	}

	@Override
	public void mouseExited(MouseEvent e) {
		
		
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		
		
	}
	
	public double getMovementSpeed() {
		return movementSpeed;
	}

	public void setMovementSpeed(double movementSpeed) {
		this.movementSpeed = movementSpeed;
	}

	public double getRotationSpeed() {
		return rotationSpeed;
	}

	public void setRotationSpeed(double rotationSpeed) {
		this.rotationSpeed = rotationSpeed;
	}
}
